package ags.muse.gun;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import ags.muse.base.Rules;
import ags.muse.base.actors.GunActor;
import ags.muse.recon.*;
import ags.util.points.*;
import ags.utils.newtree.KdTree;

public class Hypothermia {
    // Constant settings
    private static final int maxNearestCount = 108;
    private static final int maxBotNearestCount = 12;

    // Data storage
    private static HashMap<String, KdTree<RobotHistory>> storage = new HashMap<String, KdTree<RobotHistory>>();

    // Running variables
    private RobotList robots;
    private Rules rules;

    public Hypothermia(Rules rules, RobotList robots) {
        this.rules = rules;
        this.robots = robots;
    }

    double bulletPower = 0;
    public void run(GunActor actor) {
        // Abort if no enemies
        if (robots.getEnemies().size() == 0) {
            return;
        }

        // Record data
        for (EnemyRobot e : robots.getEnemies())  {
            recordData(e);
        }

        if (bulletPower != 0 && actor.getTurnRemaining() == 0 && robots.getSelf().getGunHeat() == 0) {
            actor.setFire(bulletPower);
        }
        else if (robots.getSelf().getGunHeat() <= 3*rules.GUN_COOLING_RATE) {
            glacierEvalBulletPower();

            RadialAimProfile profile = predictRobots(robots.getSelf().getLocation(), rules.getBulletSpeed(bulletPower));
            double turn = robocode.util.Utils.normalRelativeAngle(profile.getBestAngle() - robots.getSelf().getGunHeading());
            actor.setTurnGun(turn);
        }
        else {
            bulletPower = 0;
        }
        
        //System.out.println("TShadow: "+tshadow/100000000.0+"     Tbft: "+tbft/1000000000.0);
    }

    public void glacierEvalBulletPower() {
        double maxEnergy = 0;
        for (EnemyRobot e : robots.getEnemies()) {
            if (e.getEnergy() > maxEnergy) maxEnergy =  e.getEnergy();
        }

        bulletPower = Math.min(robots.getSelf().getEnergy()/6, 1300/robots.getSelf().getClosestDist());
        bulletPower = Math.min(bulletPower, rules.damageToBulletPower(maxEnergy));
        bulletPower = Math.max(0.1, Math.min(bulletPower, 2.999));
        bulletPower = Math.min(bulletPower, robots.getSelf().getEnergy());
    }

    /**
     * Ported from Diamond
     */
    public void diamondEvalBulletPower() {
        double minDist = Double.POSITIVE_INFINITY;
        EnemyRobot closestBot = null;
        for (EnemyRobot bot : robots.getEnemies()) {
            double dist = bot.getDistance(robots.getSelf().getName());
            if (dist < minDist) {
                minDist = dist;
                closestBot = bot;
            }
        }

        if (closestBot == null) return;
        double myEnergy = robots.getSelf().getEnergy();

        if (robots.getEnemies().size() == 1) { 
            bulletPower = 1.999;

            //                double gunRating = (is1v1() ? 
            //                    _virtualGuns.getRating(_currentGun, _opponentName) : 0);
            //                int shotsFired = (is1v1() ?
            //                    _virtualGuns.getShotsFired(_currentGun, _opponentName) : 0);
            if (minDist < 250) {
                bulletPower = 2.499;
            }

            if (minDist < 150) {
                bulletPower = 2.999;
            }

            if (minDist > 700) {
                bulletPower = 1.499;
            }

            if (myEnergy < closestBot.getEnergy() - 5 &&
                    myEnergy < 25 && minDist > 400) {
                bulletPower = Math.min(bulletPower, 2 - 
                        ((25 - myEnergy) / 11));
            } else if (myEnergy < 25 && minDist > 250) {
                bulletPower = Math.min(bulletPower, 2 - 
                        ((25 - myEnergy) / 12));
            }
            bulletPower = Math.min(bulletPower, closestBot.getEnergy() / 4);
            bulletPower = Math.min(bulletPower, myEnergy);
            bulletPower = Math.min(bulletPower, 3.0);
            bulletPower = Math.max(bulletPower, 0.1);
        } else {
            int enemiesAlive = robots.getEnemies().size();
            double avgEnemyEnergy = 0;
            for (EnemyRobot r : robots.getEnemies()) {
                avgEnemyEnergy += r.getEnergy();
            }
            avgEnemyEnergy /= enemiesAlive;

            bulletPower = 2.999;

            if (enemiesAlive <= 3) {
                bulletPower = 1.999;
            }

            if (enemiesAlive <= 5 && minDist > 500) {
                bulletPower = 1.499;
            }

            if ((myEnergy < avgEnemyEnergy && enemiesAlive <= 5 && 
                    minDist > 300) || minDist > 700) {

                bulletPower = 0.999;
            }

            if (myEnergy < 20 && myEnergy < avgEnemyEnergy) {
                bulletPower = Math.min(bulletPower, 2 - 
                        ((20 - myEnergy) / 11));
            }

            bulletPower = Math.min(bulletPower, myEnergy);
            bulletPower = Math.min(bulletPower, 3.0);
            bulletPower = Math.max(bulletPower, 0.1);
        }
    }

    public void paintTest(Graphics2D g) {
        if (bulletPower == 0) return;
        RadialAimProfile profile = predictRobots(robots.getSelf().getNextLocation(), rules.getBulletSpeed(bulletPower));
        g.setColor(Color.orange);
        AbsolutePoint o = robots.getSelf().getNextLocation();
        g.drawPolygon(profile.getGraphPolygon(o.x, o.y, 200));
    }

    private RadialAimProfile predictRobots(AbsolutePoint origin, double bulletSpeed) {
        ArrayList<AbsolutePoint> points = new ArrayList<AbsolutePoint>();

        int nearestCount = Math.min(maxBotNearestCount, maxNearestCount / Math.max(1, robots.getEnemies().size()));
        for (EnemyRobot robot : robots.getEnemies()) {
            points.addAll(predictRobot(origin, bulletSpeed, robot, nearestCount));
        }

        RadialAimProfile profile = new RadialAimProfile();
        for (AbsolutePoint p : points) {
            RelativePoint r = RelativePoint.fromPP(origin, p);
            double width = Math.atan(18/r.magnitude) * 2;
            double angle = r.direction;
            profile.add(angle, width, 1.0/p.distance(origin));
        }

        return profile;
    }

    private List<AbsolutePoint> predictRobot(AbsolutePoint origin, double bulletSpeed, EnemyRobot robot, int count) {
        if (robot.getEnergy() == 0) {
            return Collections.singletonList(robot.getLocation());
        }

        List<KdTree.Entry<RobotHistory>> situations = getSimilarSituations(robot, count);
        Collections.reverse(situations);

        ArrayList<AbsolutePoint> points = new ArrayList<AbsolutePoint>(situations.size());
        for (KdTree.Entry<RobotHistory> entry : situations) {
/*
                tbft -= System.nanoTime(); for (int j = 0; j<10; j++) {
            AbsolutePoint p2 = predictSituation2(origin, bulletSpeed, robot, entry.value);
                } tbft += System.nanoTime();
                tshadow -= System.nanoTime(); for (int j = 0; j<10; j++) {
                    AbsolutePoint p = predictSituation2(origin, bulletSpeed, robot, entry.value);
                        } tshadow += System.nanoTime();
*/
            AbsolutePoint p = predictSituation(origin, bulletSpeed, robot, entry.value);
            if (p != null) {
                points.add(p);
                if (points.size() >= count) {
                    break;
                }
            }
        }

        if (points.size() == 0) {
            return Collections.singletonList(robot.getLocation());
        }

        return points;
    }

    //private static long tshadow = 0;
    private AbsolutePoint predictSituation(AbsolutePoint origin, double bulletSpeed, EnemyRobot robot, RobotHistory situation) {
        // Constants
        double rotation = situation.getVelocity().direction - robot.getVelocity().direction;
        if (robot.getHistory().getOrientation() != situation.getOrientation()) {
            rotation += Math.PI;
        }
        long timeTillFiring = (long) Math.ceil(robots.getSelf().getGunHeat()/rules.GUN_COOLING_RATE);
        RelativePoint relativeLocation = RelativePoint.fromPP(robot.getLocation(), origin);
        AbsolutePoint projPoint = situation.getLocation().project(relativeLocation.direction + rotation, relativeLocation.magnitude);

        // Non-constants
        RobotHistory cursor = situation;
        double dist;
        double bulletDist = bulletSpeed * (-robot.getDataAge() - timeTillFiring);

        // Loop while bullet not passed yet
        do {
            cursor = cursor.next();
            
            if (cursor == null) return null;

            // Recalculate distance
            dist = cursor.getLocation().distanceSq(projPoint);
            bulletDist += bulletSpeed;
        } while ((bulletDist < 0) || (bulletDist*bulletDist) < dist);

        // Get the projected point
        double angle = projPoint.angleTo(cursor.getLocation());
        AbsolutePoint target = origin.project(angle - rotation, Math.sqrt(dist)); 
        
        // Check that the projected point is within map bounds
        if (target.x < 17.9 || target.x > rules.BATTLEFIELD_WIDTH-35.8 || target.y < 17.9 || target.y > rules.BATTLEFIELD_HEIGHT-35.8) {
            return null;
        }
        return target;
    }
    
    //private static long tbft = 0;
    private AbsolutePoint predictSituation2(AbsolutePoint origin, double bulletSpeed, EnemyRobot robot, RobotHistory situation) {
        // Constants
        double rotation = situation.getVelocity().direction - robot.getVelocity().direction;
        if (robot.getHistory().getOrientation() != situation.getOrientation()) {
            rotation += Math.PI;
        }
        long timeTillFiring = (long) Math.ceil(robots.getSelf().getGunHeat()/rules.GUN_COOLING_RATE);
        RelativePoint relativeLocation = RelativePoint.fromPP(robot.getLocation(), origin);
        AbsolutePoint projPoint = situation.getLocation().project(relativeLocation.direction + rotation, relativeLocation.magnitude);

        // Non-constants
        long t = -robot.getDataAge() - timeTillFiring;
        RobotHistory cursor = situation;
        double dist = relativeLocation.magnitude;
        long bft = (long) Math.ceil(dist / bulletSpeed);
        long lastt = t;

        // Loop while the elapsed time is not the bullet flight time
        while (bft != t && !(bft == lastt && t > bft)) {
            lastt = t;
            if (bft < t) {
                // Reverse skip
                while (cursor.prev() != null && t > bft) {
                    cursor = cursor.prev();
                    t--; // t += cursor.getTime() - cursor.next().getTime();
                }
            }
            else {
                // Forward skip
                if (cursor.next() == null) {
                    return null;
                }
                while (cursor.next() != null && t < bft) {
                    cursor = cursor.next();
                    t++; // t += cursor.getTime() - cursor.prev().getTime();
                }
            }

            // Recalculate bft
            dist = cursor.getLocation().distance(projPoint);
            bft = (long) Math.ceil(dist / bulletSpeed);
        }

        // Get the projected point
        double angle = projPoint.angleTo(cursor.getLocation());
        AbsolutePoint target = origin.project(angle - rotation, dist); 

        // Check that the projected point is within map bounds
        if (target.x < 18 || target.x > rules.BATTLEFIELD_WIDTH-36 || target.y < 18 || target.y > rules.BATTLEFIELD_HEIGHT-36) {
            return null;
        }
        return target;
    }

    private List<KdTree.Entry<RobotHistory>> getSimilarSituations(EnemyRobot robot, int count) {
        KdTree<RobotHistory> tree = storage.get(robot.getName());
        if (tree == null) {
            return Collections.emptyList();
        }

        double[] location = getPosition(robots, robot);
        return tree.nearestNeighbor(location, count*4, true);
    }

    private void recordData(EnemyRobot robot) {
        if (robot.getDataAge() != 1) {
            return;
        }

        if (robot.getEnergy() == 0) {
            return;
        }

        double[] location = getPosition(robots, robot);
        KdTree<RobotHistory> tree = storage.get(robot.getName());
        if (tree == null) {
            tree = new KdTree.SqrEuclid<RobotHistory>(dimensions, null);
            storage.put(robot.getName(), tree);
        }
        tree.addPoint(location, robot.getHistory());
    }

    private static final int dimensions = 12;
    private double[] getPosition(RobotList robots, EnemyRobot robot) {
        double[] p = new double[dimensions];

        // Velocity dimension
        p[0] = Math.abs(robot.getVelocity().magnitude)*(2.0/8.0);

        // Acceleration dimension
        RobotHistory prev = robot.getHistory().prev();
        if (prev != null) {
            p[1] = (robot.getHistory().prev().getVelocity().magnitude * robot.getVelocity().magnitude * robot.getHistory().getOrientation())*(1.0/2.0);
        } else {
            p[1] = 0;
        }

        // Proximity checks
        p[2] = 3.0/(closestAngledDist(robot, robots.getRobots(), 0));
        p[3] = 3.0/(closestAngledDist(robot, robots.getRobots(), Math.PI/4));
        p[4] = 3.0/(closestAngledDist(robot, robots.getRobots(), -Math.PI/4));

        p[5] = 3.0/(closestAngledDist(robot, robots.getRobots(), Math.PI));
        p[6] = 2.0/(closestAngledDist(robot, robots.getRobots(), Math.PI+Math.PI/4));
        p[7] = 2.0/(closestAngledDist(robot, robots.getRobots(), Math.PI-Math.PI/4));
        /*p[2] = 3.0/(closestAngledDist(robot, robots.getRobots(), 0));
        p[3] = 3.0/(closestAngledDist(robot, robots.getRobots(), Math.PI/2));
        p[4] = 3.0/(closestAngledDist(robot, robots.getRobots(), Math.PI));
        p[5] = 3.0/(closestAngledDist(robot, robots.getRobots(), 3*Math.PI/2));*/

        // Wall checks
        p[8] = 10.0/(wallDist(robot, 0));
        p[9] = 5.0/(wallDist(robot, Math.PI));

        // Dist last 10
        RobotHistory cursor = robot.getHistory(); 
        for (int i = 0; i < 10 && cursor.prev() != null; i++) {
            cursor = cursor.prev();
        }
        p[10] = 0.05 * cursor.getLocation().distance(robot.getLocation()) / 80;

        // Dist last 20
        for (int i = 0; i < 10 && cursor.prev() != null; i++) {
            cursor = cursor.prev();
        }
        p[11] = 0.05 * cursor.getLocation().distance(robot.getLocation()) / 160;

        return p;
    }

    private double wallDist(Robot r, double angleOffset) {
        if (r.getHistory().getOrientation() == -1) angleOffset += Math.PI;
        double refdir = r.getVelocity().direction + angleOffset;
        AbsolutePoint l = r.getLocation();
        double d1 = angledDist(l, AbsolutePoint.fromXY(0, l.y), refdir); // Left
        double d2 = angledDist(l, AbsolutePoint.fromXY(rules.BATTLEFIELD_WIDTH, l.y), refdir); // Right
        double d3 = angledDist(l, AbsolutePoint.fromXY(l.x, 0), refdir); // Bottom
        double d4 = angledDist(l, AbsolutePoint.fromXY(l.x, rules.BATTLEFIELD_HEIGHT), refdir); // Top
        return Math.min(Math.min(d1, d2), Math.min(d3, d4));
    }

    private static double closestAngledDist(Robot r1, Collection<Robot> robots, double angleOffset) {
        if (r1.getHistory().getOrientation() == -1) angleOffset += Math.PI;
        double closestDist = Double.POSITIVE_INFINITY;
        for (Robot r2 : robots) {
            if (r2 == r1) continue;
            double dist = angledDist(r1.getLocation(), r2.getLocation(), r1.getVelocity().direction + angleOffset);
            if (dist < closestDist) closestDist = dist;
        }
        return closestDist;
    }

    private static double angledDist(AbsolutePoint p1, AbsolutePoint p2, double refdir) {
        RelativePoint rel = RelativePoint.fromPP(p1, p2);
        double a = Math.abs(robocode.util.Utils.normalRelativeAngle(rel.direction - refdir));
        if (a >= Math.PI/2) return Double.MAX_VALUE; 
        return rel.magnitude / Math.cos(a);
    }
}
